﻿
package extremefx.modules.seaDragon {
	import extremefx.tools.Callback;	
	
	import flash.utils.Dictionary;	
	
	import extremefx.geom.XPoint;
	import extremefx.geom.Size;
	
	import flash.display.DisplayObject;
	import flash.display.Sprite;
	import flash.geom.Rectangle;	

	/**
	 * @author Marcelo Volmaro
	 */
	public class Drawer {

		private var _source:DziTileSource;
		private var _minLevel:uint;
		private var _maxLevel:uint;
		private var _normHeight:Number;
		private var _cacheNumTiles:Array;
		private var _cachePixelRatios:Array;
		private var _tilesMatrix:Array;
		
		private var _coverage:Array;
		private var _overlays:Vector.<Overlay>;
		private var _lastDrawn:Dictionary;
		private var _updateAgain:Boolean;
		private var _viewport:Viewport;
		private var _overs:Sprite;
		private var _canvas:Sprite;
		private var _lastOverlays:Dictionary;

		public function Drawer() {
			_cacheNumTiles = [];
			_cachePixelRatios = [];
			_coverage = [];
			_overlays = new Vector.<Overlay>();
			_lastDrawn = new Dictionary();
			_lastOverlays = new Dictionary();
			_tilesMatrix = [];
			
			_updateAgain = true;
		}
		
		public function init(pTileSource:DziTileSource, pViewPort:Viewport, pContext:Sprite):void {
			_source = pTileSource;
			_viewport = pViewPort;
			_canvas = pContext;
			
			_minLevel = pTileSource.minLevel;
			_maxLevel = pTileSource.maxLevel;
			
			var s:Sprite;
			for (var i:uint = _minLevel; i <= _maxLevel; ++i){
				s = new Sprite();
				s.mouseChildren = s.mouseEnabled = s.tabEnabled = s.tabChildren = false;
				pContext.addChild(s);
			}
			
			_overs = new Sprite();
			pContext.addChild(_overs);
			
			_normHeight = pTileSource.height / pTileSource.width;
		}
		
		public function get needsUpdate():Boolean{
			return _updateAgain;
		}
		
		private function _getNumTiles(pLevel:uint):Size {
			if (!_cacheNumTiles[pLevel]) _cacheNumTiles[pLevel] = _source.getNumTiles(pLevel);
			return _cacheNumTiles[pLevel];
		}
		
		private function _getPixelRatio(pLevel:uint):XPoint {
			if (!_cachePixelRatios[pLevel]) _cachePixelRatios[pLevel] = _source.getPixelRatio(pLevel);
			return _cachePixelRatios[pLevel];
		}
		
		private function _getTile(pLevel:uint, pX:uint, pY:uint):Tile {
			if (!_tilesMatrix[pLevel]) {
				_tilesMatrix[pLevel] = [];
			}
			
			if (!_tilesMatrix[pLevel][pX]) {
				_tilesMatrix[pLevel][pX] = [];
			}
			
			var tile:Tile;
			if (!_tilesMatrix[pLevel][pX][pY]) {
				var bounds:Rectangle = _source.getTileBounds(pLevel, pX, pY);
				var exists:Boolean = _source.tileExists(pLevel, pX, pY);
				var url:String = _source.getTileUrl(pLevel, pX, pY);
				
				tile = new Tile(pLevel, pX, pY, bounds, exists, url);
				tile.tileSize = _source.tileSize;
				tile.overlap = _source.tileOverlap;
				_tilesMatrix[pLevel][pX][pY] = tile; 
			}
			
			tile = _tilesMatrix[pLevel][pX][pY];
			tile.touch();
			return tile;
		}
		
		private function _providesCoverage(pLevel:uint, pX:int = -1, pY:int = -1):Boolean {
			if (!_coverage[pLevel]) {
				return false;
			}
			
			if (pX == -1 || pY == -1) {
				for each(var row:Array in _coverage[pLevel]){
					for each(var col:Boolean in row){
						if (!col) return false;
					}
				}
				
				return true;
			}
			
			return (_coverage[pLevel][pX] === undefined || 
					_coverage[pLevel][pX][pY] === undefined || 
					_coverage[pLevel][pX][pY] === true);
			
		}
		
		private function _isCovered(pLevel:uint, pX:int, pY:int):Boolean {
			++pLevel;
			
			if (pX == -1 || pY == -1) {
				return _providesCoverage(pLevel);
				
			} else {
				return (	_providesCoverage(pLevel, 2 * pX, 2 * pY) && 
							_providesCoverage(pLevel, 2 * pX, 2 * pY + 1) && 
							_providesCoverage(pLevel, 2 * pX + 1, 2 * pY) && 
							_providesCoverage(pLevel, 2 * pX + 1, 2 * pY + 1));
			}
		}
		
		private function _setCoverage(pLevel:uint, pX:Number, pY:Number, pCovers:Boolean):void {
			if (!_coverage[pLevel]) {
				trace("Setting coverage for a tile before its " + "level's coverage has been reset: " + pLevel);
				return;
			}
			
			if (!_coverage[pLevel][pX]) {
				_coverage[pLevel][pX] = [];
			}
			
			_coverage[pLevel][pX][pY] = pCovers;
		}
		
		private function _resetCoverage(pLevel:uint):void {
			_coverage[pLevel] = [];
		}
		
		private function _getOverlayIndex(pElement:DisplayObject):int {
			var i:int = 0;
			for each(var o:Overlay in _overlays){
				if (o._element == pElement) {
					return i;
				}
				
				++i;
			}
			
			return -1;
		}
		
		public function update():void {
			_updateAgain = false;
			
			var viewportBounds:Rectangle = _viewport.getBounds(true);
			
			if (isNaN(viewportBounds.x) || isNaN(viewportBounds.y) || isNaN(viewportBounds.width) || isNaN(viewportBounds.height)) return;
			
			var viewportTL:XPoint = new XPoint(viewportBounds.x, viewportBounds.y);
			var viewportBR:XPoint = new XPoint(viewportBounds.x + viewportBounds.width, viewportBounds.y + viewportBounds.height);
			
			if (viewportBR.x < 0 || viewportBR.y < 0 || viewportTL.x > 1 || viewportTL.y > _normHeight) {
				return;
			}
			
			var tile:Tile;
			var newDrawn:Dictionary = new Dictionary();
			
			var immediateRender:Boolean = Config.immediateRender;
			
			viewportTL.x = Math.max(viewportTL.x, 0);
			viewportTL.y = Math.max(viewportTL.y, 0);
			viewportBR.x = Math.min(viewportBR.x, 1);
			viewportBR.y = Math.min(viewportBR.y, _normHeight);
			
			var best:Tile = null;
			var haveDrawn:Boolean = false;
			var viewportCenter:XPoint = _viewport.pixelFromPoint(_viewport.getCenter());
			
			var zeroRatioT:Number = _viewport.deltaPixelsFromPoints(_getPixelRatio(0), false).x;
			
			var optimalPixelRatio:Number = immediateRender ? 1 : zeroRatioT;
			
//			trace(Math.floor(Math.log(64) / Math.log(2)))
			
			var lowestLevel:Number = Math.max(_minLevel, 6);
			
			var zeroRatioC:Number = _viewport.deltaPixelsFromPoints(_getPixelRatio(0), true).x;
			var highestLevel:Number = Math.min(_maxLevel, Math.floor(Math.log(zeroRatioC / 0.5) / Math.log(2)));
			lowestLevel = Math.min(lowestLevel, highestLevel);
			
			for (var level:Number = highestLevel; level >= lowestLevel; level--) {
				var drawLevel:Boolean = false;
				var renderPixelRatioC:Number = _viewport.deltaPixelsFromPoints(_getPixelRatio(level), true).x;
				
				if ((!haveDrawn && renderPixelRatioC >= 0.5) || level == lowestLevel) {
					drawLevel = true;
					haveDrawn = true;
					
				} else if (!haveDrawn) {
					continue;
				}
				
				_resetCoverage(level);
				
				var renderPixelRatioT:Number = _viewport.deltaPixelsFromPoints(_getPixelRatio(level), false).x;
				
				var levelVisibility:Number = optimalPixelRatio / Math.abs(optimalPixelRatio - renderPixelRatioT);
				
				var tileTL:XPoint = _source.getTileAtPoint(level, viewportTL);
				var tileBR:XPoint = _source.getTileAtPoint(level, viewportBR);
				
				var numTiles:Size = _getNumTiles(level);
				
				var numX:Number = Math.min(tileBR.x, numTiles.width - 1);
				var numY:Number = Math.min(tileBR.y, numTiles.height - 1);
				
				for (var x:uint = tileTL.x; x <= numX; ++x) {
					for (var y:uint = tileTL.y; y <= numY; ++y) {
						tile = _getTile(level, x, y);
						
						var drawTile:Boolean = drawLevel;
						_setCoverage(level, x, y, false);
						
						if (!tile._exists) {
							continue;
						}
						
						if (haveDrawn && !drawTile) {
							if (_isCovered(level, x, y)) {
								_setCoverage(level, x, y, true);
								
							} else {
								drawTile = true;
							}
						}
						
						if (!drawTile) {
							continue;
						}
						
						var boundsTL:XPoint = new XPoint(tile.bounds.x, tile.bounds.y);
						var boundsSize:XPoint = new XPoint(tile.bounds.width, tile.bounds.height);
						
						var positionC:XPoint = _viewport.pixelFromPoint(boundsTL, true);
						var sizeC:XPoint = _viewport.deltaPixelsFromPoints(boundsSize, true);
				
						var positionT:XPoint = _viewport.pixelFromPoint(boundsTL, false);
						var sizeT:XPoint = _viewport.deltaPixelsFromPoints(boundsSize, false);
						var tileCenter:XPoint = positionT.plus(sizeT.divide(2));
						var tileDistance:Number = viewportCenter.distanceTo(tileCenter);
						
						tile._distance = tileDistance;
						tile._visibility = levelVisibility;
						tile.setBounds(positionC, sizeC);
						
						if (tile._loaded) {
							newDrawn[tile] = tile;
							tile.draw();
							
						} else if (!tile._loading) {
							best = tile.compareTo(best) == 1 ? tile : best;
							best.alphaDone = Callback.create(_tileLoaded, level, x, y); 
						}
					}
				}
				
				if (_providesCoverage(level)) {
					break;
				}
			}
			
			for each(var ltile:Tile in _lastDrawn){
				if (newDrawn[ltile]) continue;
				ltile.remove();
			}
			
			_lastDrawn = newDrawn;
			
			var r:Rectangle = _viewport.getBounds();
			var newOverlays:Dictionary = new Dictionary();
			
			for each(var o:Overlay in _overlays){
				var bounds:Rectangle = o._bounds;
				if (r.intersects(bounds)){
					newOverlays[o] = o;

					o.setBounds(_overs,
								_viewport.pixelFromPoint(new XPoint(bounds.x, bounds.y), true), 
								_viewport.deltaPixelsFromPoints(new XPoint(bounds.width, bounds.height), true));
				}
			}
			
			for each(var u:Overlay in _lastOverlays){
				if (newOverlays[u]) continue;
				u.dispose();
			}
			
			_lastOverlays = newOverlays;
			
			if (best) {
				best.load(_canvas);
				_updateAgain = true;
			}
		}
		
		private function _tileLoaded(pLevel:uint, pX:uint, pY:uint, ...args):void {
			_setCoverage(pLevel, pX, pY, true);
			_updateAgain = true;
		}

		public function addOverlay(pDisplayObject:DisplayObject, pLocation:Rectangle, pPlacement:uint = 0):void {
			if (_getOverlayIndex(pDisplayObject) != -1) {
				return;
			}
			
			_overlays.push(new Overlay(pDisplayObject, pLocation, pPlacement));
			_updateAgain = true;
		}
		
		public function updateOverlay(pDisplayObject:DisplayObject, pLocation:Rectangle, pPlacement:uint = 0):void {
			var i:int = _getOverlayIndex(pDisplayObject); 
			if (i == -1) {
				return;
			}
			
			_overlays[i].update(pLocation, pPlacement);
			_updateAgain = true;
		}
		
		public function removeOverlay(pDisplayObject:DisplayObject):void {
			var i:int = _getOverlayIndex(pDisplayObject); 
			if (i == -1) {
				return;
			}
			
			_overlays[i].dispose();
			_overlays.splice(i, 1);
			_updateAgain = true;
		}
		
		public function clearOverlays():void {
			for each(var o:Overlay in _overlays){
				o.dispose();
			}
			
			_overlays = new Vector.<Overlay>();
			_updateAgain = true;
		}
	}
}
